[WIP] Call UI

This commit is contained in:
Isaac 2023-12-01 01:06:22 +04:00
parent 835278b9dc
commit 06e3841f23
22 changed files with 1204 additions and 480 deletions

View File

@ -10575,12 +10575,19 @@ Sorry for the inconvenience.";
"ChannelReactions.UnsavedChangesAlertDiscard" = "Discard";
"ChannelReactions.UnsavedChangesAlertApply" = "Apply";
"ChannelReactions.ToastMaxReactionsReached" = "You can select at most 100 reactions.";
"ChannelReactions.ToastLevelBoostRequired" = "Your channel needs to reach **Level %1$@** to add **%2$@** custom emoji as reactions.**";
"ChannelReactions.ToastLevelBoostRequiredTemplate" = "Your channel needs to reach **%1$@** to add **%2$@** as reactions.**";
"ChannelReactions.ToastLevelBoostRequiredTemplateLevel_1" = "Level 1";
"ChannelReactions.ToastLevelBoostRequiredTemplateLevel_any" = "Level %d";
"ChannelReactions.ToastLevelBoostRequiredTemplateEmojiCount_1" = "1 custom emoji";
"ChannelReactions.ToastLevelBoostRequiredTemplateEmojiCount_any" = "%d custom emoji";
"ChannelReactions.GeneralInfoLabel" = "You can add emoji from any emoji pack as a reaction.";
"ChannelReactions.ReactionsSectionTitle" = "AVAILABLE REACTIONS";
"ChannelReactions.ReactionsInfoLabel" = "You can also [create your own]() emoji packs and use them.";
"ChannelReactions.SaveAction" = "Update Reactions";
"ChannelReactions.LevelRequiredLabel" = "Level %1$@ Required";
"ChannelReactions.InputPlaceholder" = "Add Reactions...";
"ProfileColorSetup.ResetAction" = "Reset Profile Color";
"ProfileColorSetup.IconSectionTitle" = "ADD ICON TO PROFILE";

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -16,6 +16,8 @@ public final class AppDelegate: NSObject, UIApplicationDelegate {
window.rootViewController = ViewController()
window.makeKeyAndVisible()
application.internalSetStatusBarStyle(.lightContent, animated: false)
return true
}
}

View File

@ -5,7 +5,7 @@ import Display
import CallScreen
import ComponentFlow
public final class ViewController: UIViewController {
public final class ViewController: UIViewController {
private var callScreenView: PrivateCallScreen?
private var callState: PrivateCallScreen.State = PrivateCallScreen.State(
lifecycleState: .connecting,
@ -17,6 +17,9 @@ public final class ViewController: UIViewController {
remoteVideo: nil
)
private var currentLayout: (size: CGSize, insets: UIEdgeInsets)?
private var viewLayoutTransition: Transition?
override public func viewDidLoad() {
super.viewDidLoad()
@ -66,6 +69,8 @@ public final class ViewController: UIViewController {
}
if let input = self.callState.localVideo as? FileVideoSource {
input.sourceId = input.sourceId == 0 ? 1 : 0
input.fixedRotationAngle = input.sourceId == 0 ? Float.pi * 0.0 : Float.pi * 0.5
input.sizeMultiplicator = input.sourceId == 0 ? CGPoint(x: 1.0, y: 1.0) : CGPoint(x: 1.5, y: 1.0)
}
}
callScreenView.videoAction = { [weak self] in
@ -73,7 +78,7 @@ public final class ViewController: UIViewController {
return
}
if self.callState.localVideo == nil {
self.callState.localVideo = FileVideoSource(device: MetalEngine.shared.device, url: Bundle.main.url(forResource: "test2", withExtension: "mp4")!)
self.callState.localVideo = FileVideoSource(device: MetalEngine.shared.device, url: Bundle.main.url(forResource: "test3", withExtension: "mp4")!, fixedRotationAngle: Float.pi * 0.0)
} else {
self.callState.localVideo = nil
}
@ -81,7 +86,7 @@ public final class ViewController: UIViewController {
}
callScreenView.microhoneMuteAction = {
if self.callState.remoteVideo == nil {
self.callState.remoteVideo = FileVideoSource(device: MetalEngine.shared.device, url: Bundle.main.url(forResource: "test2", withExtension: "mp4")!)
self.callState.remoteVideo = FileVideoSource(device: MetalEngine.shared.device, url: Bundle.main.url(forResource: "test4", withExtension: "mp4")!, fixedRotationAngle: Float.pi * 0.0)
} else {
self.callState.remoteVideo = nil
}
@ -96,32 +101,44 @@ public final class ViewController: UIViewController {
self.callState.localVideo = nil
self.update(transition: .spring(duration: 0.4))
}
self.update(transition: .immediate)
}
private func update(transition: Transition) {
self.update(size: self.view.bounds.size, transition: transition)
if let (size, insets) = self.currentLayout {
self.update(size: size, insets: insets, transition: transition)
}
}
private func update(size: CGSize, transition: Transition) {
private func update(size: CGSize, insets: UIEdgeInsets, transition: Transition) {
guard let callScreenView = self.callScreenView else {
return
}
transition.setFrame(view: callScreenView, frame: CGRect(origin: CGPoint(), size: size))
let insets: UIEdgeInsets
if size.width < size.height {
insets = UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0)
} else {
insets = UIEdgeInsets(top: 0.0, left: 44.0, bottom: 0.0, right: 44.0)
}
callScreenView.update(size: size, insets: insets, screenCornerRadius: 55.0, state: self.callState, transition: transition)
}
override public func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
let safeAreaLayoutGuide = self.view.safeAreaLayoutGuide
let size = self.view.bounds.size
let insets = UIEdgeInsets(top: safeAreaLayoutGuide.layoutFrame.minY, left: safeAreaLayoutGuide.layoutFrame.minX, bottom: size.height - safeAreaLayoutGuide.layoutFrame.maxY, right: safeAreaLayoutGuide.layoutFrame.minX)
let transition = self.viewLayoutTransition ?? .immediate
self.viewLayoutTransition = nil
if let currentLayout = self.currentLayout, currentLayout == (size, insets) {
} else {
self.currentLayout = (size, insets)
self.update(size: size, insets: insets, transition: transition)
}
}
override public func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
self.update(size: size, transition: .easeInOut(duration: 0.3))
self.viewLayoutTransition = .easeInOut(duration: 0.3)
}
}

View File

@ -11,8 +11,10 @@ public func chatTextInputAddFormattingAttribute(_ state: ChatTextInputState, att
state.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, _ in
for (key, _) in attributes {
if key == attribute {
addAttribute = false
attributesToRemove.append(key)
if nsRange == range {
addAttribute = false
attributesToRemove.append(key)
}
}
}
}

View File

@ -591,7 +591,7 @@ public struct Transition {
completion?(true)
case let .curve(duration, curve):
let previousValue: CATransform3D
if let presentation = layer.presentation() {
if layer.animation(forKey: "transform") != nil, let presentation = layer.presentation() {
previousValue = presentation.transform
} else {
previousValue = layer.transform
@ -703,6 +703,33 @@ public struct Transition {
)
}
}
public func setZPosition(layer: CALayer, zPosition: CGFloat, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
if layer.zPosition == zPosition {
completion?(true)
return
}
switch self.animation {
case .none:
layer.zPosition = zPosition
layer.removeAnimation(forKey: "zPosition")
completion?(true)
case let .curve(duration, curve):
let previousZPosition = layer.presentation()?.opacity ?? layer.opacity
layer.zPosition = zPosition
layer.animate(
from: previousZPosition as NSNumber,
to: zPosition as NSNumber,
keyPath: "zPosition",
duration: duration,
delay: delay,
curve: curve,
removeOnCompletion: true,
additive: false,
completion: completion
)
}
}
public func animateScale(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, delay: Double = 0.0, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
switch self.animation {

View File

@ -203,9 +203,9 @@ public extension CALayer {
}
}
func animate(from: AnyObject?, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
func animate(from: AnyObject?, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil, key: String? = nil) {
let animation = self.makeAnimation(from: from, to: to, keyPath: keyPath, timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
self.add(animation, forKey: additive ? nil : keyPath)
self.add(animation, forKey: key ?? (additive ? nil : keyPath))
}
func animateGroup(_ animations: [CAAnimation], key: String, completion: ((Bool) -> Void)? = nil) {

View File

@ -87,3 +87,32 @@ open class SimpleGradientLayer: CAGradientLayer {
fatalError("init(coder:) has not been implemented")
}
}
open class SimpleTransformLayer: CATransformLayer {
public var didEnterHierarchy: (() -> Void)?
public var didExitHierarchy: (() -> Void)?
public private(set) var isInHierarchy: Bool = false
override open func action(forKey event: String) -> CAAction? {
if event == kCAOnOrderIn {
self.isInHierarchy = true
self.didEnterHierarchy?()
} else if event == kCAOnOrderOut {
self.isInHierarchy = false
self.didExitHierarchy?()
}
return nullAction
}
override public init() {
super.init()
}
override public init(layer: Any) {
super.init(layer: layer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -115,6 +115,10 @@ open class MetalEngineSubjectLayer: SimpleLayer {
fileprivate var internalId: Int = -1
fileprivate var surfaceAllocation: MetalEngine.SurfaceAllocation?
#if DEBUG
fileprivate var surfaceChangeFrameCount: Int = 0
#endif
public override init() {
super.init()
@ -780,7 +784,10 @@ public final class MetalEngine {
if previousSurfaceId != nil {
#if DEBUG
print("Changing surface for layer \(layer) (\(renderSpec.allocationWidth)x\(renderSpec.allocationHeight))")
layer.surfaceChangeFrameCount += 1
if layer.surfaceChangeFrameCount > 100 {
print("Changing surface for layer \(layer) (\(renderSpec.allocationWidth)x\(renderSpec.allocationHeight))")
}
#endif
}
} else {
@ -792,6 +799,10 @@ public final class MetalEngine {
#endif
}
}
} else {
#if DEBUG
layer.surfaceChangeFrameCount = max(0, layer.surfaceChangeFrameCount - 1)
#endif
}
}

View File

@ -57,13 +57,31 @@ final class ButtonGroupView: OverlayMaskContainerView {
fatalError("init(coder:) has not been implemented")
}
func update(size: CGSize, buttons: [Button], transition: Transition) {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let result = super.hitTest(point, with: event) else {
return nil
}
if result === self {
return nil
}
return result
}
func update(size: CGSize, insets: UIEdgeInsets, controlsHidden: Bool, buttons: [Button], transition: Transition) -> CGFloat {
self.buttons = buttons
let buttonSize: CGFloat = 56.0
let buttonSpacing: CGFloat = 36.0
let buttonY: CGFloat = size.height - 86.0 - buttonSize
let buttonY: CGFloat
let resultHeight: CGFloat
if controlsHidden {
buttonY = size.height + 12.0
resultHeight = insets.bottom + 4.0
} else {
buttonY = size.height - insets.bottom - 52.0 - buttonSize
resultHeight = size.height - buttonY
}
var buttonX: CGFloat = floor((size.width - buttonSize * CGFloat(buttons.count) - buttonSpacing * CGFloat(buttons.count - 1)) * 0.5)
for button in buttons {
@ -137,5 +155,7 @@ final class ButtonGroupView: OverlayMaskContainerView {
for key in removeKeys {
self.buttonViews.removeValue(forKey: key)
}
return resultHeight
}
}

View File

@ -64,7 +64,7 @@ final class ContentOverlayButton: HighlightTrackingButton, OverlayMaskContainerV
if highlighted {
self.layer.removeAnimation(forKey: "opacity")
self.layer.removeAnimation(forKey: "sublayerTransform")
self.layer.removeAnimation(forKey: "transform")
let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut))
transition.setScale(layer: self.layer, scale: topScale)
} else {

View File

@ -0,0 +1,17 @@
import Foundation
import UIKit
import Display
final class EmojiTooltipView: UIView {
let size: CGSize
init(text: String) {
self.size = CGSize()
super.init(frame: CGRect())
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -1,144 +0,0 @@
import Foundation
import UIKit
import Display
import MetalEngine
import ComponentFlow
final class MinimizedVideoContainerView: UIView {
private struct Params: Equatable {
var size: CGSize
var insets: UIEdgeInsets
init(size: CGSize, insets: UIEdgeInsets) {
self.size = size
self.insets = insets
}
}
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 let videoLayer: PrivateCallVideoLayer
private var params: Params?
private var videoMetrics: VideoMetrics?
private var appliedVideoMetrics: VideoMetrics?
var video: VideoSource? {
didSet {
self.video?.updated = { [weak self] in
guard let self else {
return
}
var videoMetrics: VideoMetrics?
if let currentOutput = self.video?.currentOutput {
self.videoLayer.video = currentOutput
videoMetrics = VideoMetrics(resolution: CGSize(width: CGFloat(currentOutput.y.width), height: CGFloat(currentOutput.y.height)), 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))
}
}
var videoMetrics: VideoMetrics?
if let currentOutput = self.video?.currentOutput {
self.videoLayer.video = currentOutput
videoMetrics = VideoMetrics(resolution: CGSize(width: CGFloat(currentOutput.y.width), height: CGFloat(currentOutput.y.height)), 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))
}
}
}
override init(frame: CGRect) {
self.videoLayer = PrivateCallVideoLayer()
self.videoLayer.masksToBounds = true
super.init(frame: frame)
self.layer.addSublayer(self.videoLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return nil
}
private func update(transition: Transition) {
guard let params = self.params else {
return
}
self.update(params: params, transition: transition)
}
func update(size: CGSize, insets: UIEdgeInsets, transition: Transition) {
let params = Params(size: size, insets: insets)
if self.params == params {
return
}
self.params = params
self.update(params: params, transition: transition)
}
private func update(params: Params, transition: Transition) {
guard let videoMetrics = self.videoMetrics else {
return
}
var transition = transition
if self.appliedVideoMetrics == nil {
transition = .immediate
}
self.appliedVideoMetrics = videoMetrics
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 videoSize = rotatedResolution.aspectFitted(CGSize(width: 160.0, height: 160.0))
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(origin: CGPoint(x: params.size.width - params.insets.right - videoSize.width, y: params.size.height - params.insets.bottom - videoSize.height), size: videoSize)
transition.setPosition(layer: self.videoLayer, position: rotatedVideoFrame.center)
transition.setBounds(layer: self.videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoSize))
transition.setPosition(layer: self.videoLayer.blurredLayer, position: rotatedVideoFrame.center)
transition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoSize))
transition.setTransform(layer: self.videoLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
transition.setTransform(layer: self.videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
transition.setCornerRadius(layer: self.videoLayer, cornerRadius: 10.0)
self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)))
}
}

View File

@ -0,0 +1,63 @@
import Foundation
import UIKit
import Display
import ComponentFlow
final class RoundedCornersView: UIImageView {
private let color: UIColor
private var currentCornerRadius: CGFloat?
private var cornerImage: UIImage?
init(color: UIColor) {
self.color = color
super.init(image: nil)
if #available(iOS 13.0, *) {
self.layer.cornerCurve = .circular
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func applyStaticCornerRadius() {
guard let cornerRadius = self.currentCornerRadius else {
return
}
if let cornerImage = self.cornerImage, cornerImage.size.height == cornerRadius * 2.0 {
} else {
let size = CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0)
self.cornerImage = generateStretchableFilledCircleImage(diameter: size.width, color: self.color)
}
self.image = self.cornerImage
self.clipsToBounds = false
self.backgroundColor = nil
self.layer.cornerRadius = 0.0
}
func update(cornerRadius: CGFloat, transition: Transition) {
if self.currentCornerRadius == cornerRadius {
return
}
let previousCornerRadius = self.currentCornerRadius
self.currentCornerRadius = cornerRadius
if transition.animation.isImmediate {
self.applyStaticCornerRadius()
} else {
self.image = nil
self.clipsToBounds = true
self.backgroundColor = self.color
if let previousCornerRadius, self.layer.animation(forKey: "cornerRadius") == nil {
self.layer.cornerRadius = previousCornerRadius
}
transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius, completion: { [weak self] completed in
guard let self, completed else {
return
}
self.applyStaticCornerRadius()
})
}
}
}

View File

@ -3,144 +3,522 @@ import UIKit
import Display
import ComponentFlow
import MetalEngine
import SwiftSignalKit
private let shadowImage: UIImage? = {
UIImage(named: "Call/VideoGradient")?.precomposed()
}()
final class VideoContainerView: UIView {
private struct Params: Equatable {
var size: CGSize
var insets: UIEdgeInsets
var cornerRadius: CGFloat
var isMinimized: Bool
var isAnimatingOut: Bool
private final class VideoContainerLayer: SimpleLayer {
let contentsLayer: SimpleLayer
override init() {
self.contentsLayer = SimpleLayer()
init(size: CGSize, insets: UIEdgeInsets, cornerRadius: CGFloat, isMinimized: Bool, isAnimatingOut: Bool) {
self.size = size
self.insets = insets
self.cornerRadius = cornerRadius
self.isMinimized = isMinimized
self.isAnimatingOut = isAnimatingOut
}
super.init()
self.addSublayer(self.contentsLayer)
}
private struct VideoMetrics: Equatable {
var resolution: CGSize
var rotationAngle: Float
override init(layer: Any) {
self.contentsLayer = SimpleLayer()
init(resolution: CGSize, rotationAngle: Float) {
self.resolution = resolution
self.rotationAngle = rotationAngle
}
}
private let videoLayer: PrivateCallVideoLayer
let blurredContainerLayer: SimpleLayer
private let topShadowView: UIImageView
private let bottomShadowView: UIImageView
private var params: Params?
private var videoMetrics: VideoMetrics?
private var appliedVideoMetrics: VideoMetrics?
var video: VideoSource? {
didSet {
self.video?.updated = { [weak self] in
guard let self else {
return
}
var videoMetrics: VideoMetrics?
if let currentOutput = self.video?.currentOutput {
self.videoLayer.video = currentOutput
videoMetrics = VideoMetrics(resolution: CGSize(width: CGFloat(currentOutput.y.width), height: CGFloat(currentOutput.y.height)), rotationAngle: currentOutput.rotationAngle)
} else {
self.videoLayer.video = nil
}
self.videoLayer.setNeedsUpdate()
if self.videoMetrics != videoMetrics {
self.videoMetrics = videoMetrics
self.update(transition: .easeInOut(duration: 0.2))
}
}
var videoMetrics: VideoMetrics?
if let currentOutput = self.video?.currentOutput {
self.videoLayer.video = currentOutput
videoMetrics = VideoMetrics(resolution: CGSize(width: CGFloat(currentOutput.y.width), height: CGFloat(currentOutput.y.height)), rotationAngle: currentOutput.rotationAngle)
} else {
self.videoLayer.video = nil
}
self.videoLayer.setNeedsUpdate()
if self.videoMetrics != videoMetrics {
self.videoMetrics = videoMetrics
self.update(transition: .easeInOut(duration: 0.2))
}
}
}
override init(frame: CGRect) {
self.videoLayer = PrivateCallVideoLayer()
self.blurredContainerLayer = SimpleLayer()
self.topShadowView = UIImageView()
self.topShadowView.transform = CGAffineTransformMakeScale(1.0, -1.0)
self.bottomShadowView = UIImageView()
super.init(frame: frame)
self.backgroundColor = UIColor.black
self.blurredContainerLayer.backgroundColor = UIColor.black.cgColor
self.layer.addSublayer(self.videoLayer)
self.blurredContainerLayer.addSublayer(self.videoLayer.blurredLayer)
self.topShadowView.image = shadowImage
self.bottomShadowView.image = shadowImage
self.addSubview(self.topShadowView)
self.addSubview(self.bottomShadowView)
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
init(isForward: Bool) {
self.isForward = isForward
}
}
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))
}
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(params: params, transition: transition)
self.update(previousParams: params, params: params, transition: transition)
}
func update(size: CGSize, insets: UIEdgeInsets, cornerRadius: CGFloat, isMinimized: Bool, isAnimatingOut: Bool, transition: Transition) {
let params = Params(size: size, insets: insets, cornerRadius: cornerRadius, isMinimized: isMinimized, isAnimatingOut: isAnimatingOut)
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
}
self.layer.masksToBounds = true
if self.layer.animation(forKey: "cornerRadius") == nil {
self.layer.cornerRadius = self.params?.cornerRadius ?? 0.0
}
let previousParams = self.params
self.params = params
transition.setCornerRadius(layer: self.layer, cornerRadius: params.cornerRadius, completion: { [weak self] completed in
guard let self, let params = self.params, completed else {
return
}
if !params.isAnimatingOut {
self.layer.masksToBounds = false
self.layer.cornerRadius = 0.0
}
})
if let previousParams, previousParams.controlsHidden != params.controlsHidden {
self.dragPosition = nil
self.dragPositionAnimatorLink = nil
}
self.update(params: params, transition: transition)
self.update(previousParams: previousParams, params: params, transition: transition)
}
private func update(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
}
@ -151,43 +529,108 @@ final class VideoContainerView: UIView {
self.appliedVideoMetrics = videoMetrics
if params.isMinimized {
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
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
videoTransform = CATransform3DRotate(videoTransform, (flipAnimationInfo.isForward ? 1.0 : -1.0) * CGFloat.pi * 0.9999, 0.0, 1.0, 0.0)
self.videoContainerLayer.transform = videoTransform
disappearingVideoLayer.videoLayer.zPosition = 1.0
transition.setZPosition(layer: disappearingVideoLayer.videoLayer, zPosition: -1.0)
disappearingVideoLayer.videoLayer.transform = CATransform3DMakeScale(-1.0, 1.0, 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 videoSize = rotatedResolution.aspectFitted(CGSize(width: 160.0, height: 160.0))
let animateFlipDisappearingVideoLayer = animateFlipDisappearingVideo?.videoLayer
transition.setTransform(layer: self.videoContainerLayer, transform: videoLayout.videoTransform, completion: { [weak animateFlipDisappearingVideoLayer] _ in
animateFlipDisappearingVideoLayer?.removeFromSuperlayer()
})
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
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))
let rotatedVideoSize = videoIsRotated ? CGSize(width: videoSize.height, height: videoSize.width) : videoSize
let 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)
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)
transition.setPosition(layer: self.videoLayer, position: rotatedVideoFrame.center)
transition.setBounds(layer: self.videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoSize))
transition.setPosition(layer: self.videoLayer.blurredLayer, position: rotatedVideoFrame.center)
transition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoSize))
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
}
})
transition.setTransform(layer: self.videoLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
transition.setTransform(layer: self.videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
transition.setCornerRadius(layer: self.videoLayer, cornerRadius: 10.0)
self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)))
let topShadowHeight: CGFloat = floor(effectiveVideoFrame.height * 0.2)
let topShadowFrame = CGRect(origin: effectiveVideoFrame.origin, size: CGSize(width: effectiveVideoFrame.width, height: topShadowHeight))
transition.setPosition(view: self.topShadowView, position: topShadowFrame.center)
transition.setBounds(view: self.topShadowView, bounds: CGRect(origin: CGPoint(x: effectiveVideoFrame.minX, y: effectiveVideoFrame.maxY - topShadowHeight), size: topShadowFrame.size))
transition.setAlpha(view: self.topShadowView, alpha: 0.0)
let bottomShadowHeight: CGFloat = 200.0
transition.setFrame(view: self.bottomShadowView, frame: CGRect(origin: CGPoint(x: 0.0, y: params.size.height - bottomShadowHeight), size: CGSize(width: params.size.width, height: bottomShadowHeight)))
transition.setAlpha(view: self.bottomShadowView, alpha: 0.0)
self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(videoLayout.rotatedVideoResolution.width), height: Int(videoLayout.rotatedVideoResolution.height)))
} else {
var rotatedResolution = videoMetrics.resolution
var videoIsRotated = false
@ -196,41 +639,109 @@ final class VideoContainerView: UIView {
videoIsRotated = true
}
var videoSize = rotatedResolution.aspectFitted(params.size)
let boundingAspectRatio = params.size.width / params.size.height
let videoAspectRatio = videoSize.width / videoSize.height
if abs(boundingAspectRatio - videoAspectRatio) < 0.15 {
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 rotatedBoundingSize = params.size
let rotatedVideoFrame = CGRect(origin: CGPoint(x: floor((rotatedBoundingSize.width - rotatedVideoSize.width) * 0.5), y: floor((rotatedBoundingSize.height - rotatedVideoSize.height) * 0.5)), size: rotatedVideoSize)
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.videoLayer, position: rotatedVideoFrame.center)
transition.setBounds(layer: self.videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size))
transition.setPosition(layer: self.videoLayer.blurredLayer, position: rotatedVideoFrame.center)
transition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size))
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)
transition.setTransform(layer: self.videoLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
transition.setTransform(layer: self.videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
if !params.isAnimatingOut {
self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)))
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)
}
}
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(view: self.topShadowView, position: topShadowFrame.center)
transition.setBounds(view: self.topShadowView, bounds: CGRect(origin: CGPoint(), size: topShadowFrame.size))
transition.setAlpha(view: self.topShadowView, alpha: 1.0)
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()
})
}
}
let bottomShadowHeight: CGFloat = 200.0
transition.setFrame(view: self.bottomShadowView, frame: CGRect(origin: CGPoint(x: 0.0, y: params.size.height - bottomShadowHeight), size: CGSize(width: params.size.width, height: bottomShadowHeight)))
transition.setAlpha(view: self.bottomShadowView, alpha: 1.0)
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)))
}
}
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)
}
}

View File

@ -0,0 +1,17 @@
import Foundation
import UIKit
import Display
import ComponentFlow
final class VideoShadowsView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(size: CGSize, transition: Transition) {
}
}

View File

@ -2,14 +2,17 @@ import AVFoundation
import Metal
import CoreVideo
import Display
import SwiftSignalKit
public final class VideoSourceOutput {
public let resolution: CGSize
public let y: MTLTexture
public let uv: MTLTexture
public let rotationAngle: Float
public let sourceId: Int
public init(y: MTLTexture, uv: MTLTexture, rotationAngle: Float, sourceId: Int) {
public init(resolution: CGSize, y: MTLTexture, uv: MTLTexture, rotationAngle: Float, sourceId: Int) {
self.resolution = resolution
self.y = y
self.uv = uv
self.rotationAngle = rotationAngle
@ -20,8 +23,9 @@ public final class VideoSourceOutput {
public protocol VideoSource: AnyObject {
typealias Output = VideoSourceOutput
var updated: (() -> Void)? { get set }
var currentOutput: Output? { get }
func addOnUpdated(_ f: @escaping () -> Void) -> Disposable
}
public final class FileVideoSource: VideoSource {
@ -35,13 +39,17 @@ public final class FileVideoSource: VideoSource {
private var targetItem: AVPlayerItem?
public private(set) var currentOutput: Output?
public var updated: (() -> Void)?
private var onUpdatedListeners = Bag<() -> Void>()
private var displayLink: SharedDisplayLinkDriver.Link?
public var sourceId: Int = 0
public var fixedRotationAngle: Float?
public var sizeMultiplicator: CGPoint = CGPoint(x: 1.0, y: 1.0)
public init?(device: MTLDevice, url: URL) {
public init?(device: MTLDevice, url: URL, fixedRotationAngle: Float? = nil) {
self.fixedRotationAngle = fixedRotationAngle
self.device = device
CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache)
@ -62,11 +70,26 @@ public final class FileVideoSource: VideoSource {
return
}
if self.updateOutput() {
self.updated?()
for onUpdated in self.onUpdatedListeners.copyItems() {
onUpdated()
}
}
})
}
public func addOnUpdated(_ f: @escaping () -> Void) -> Disposable {
let index = self.onUpdatedListeners.add(f)
return ActionDisposable { [weak self] in
DispatchQueue.main.async {
guard let self else {
return
}
self.onUpdatedListeners.remove(index)
}
}
}
private func updateOutput() -> Bool {
if self.targetItem !== self.queuePlayer.currentItem {
self.targetItem?.remove(self.videoOutput)
@ -117,9 +140,15 @@ public final class FileVideoSource: VideoSource {
return false
}
rotationAngle = Float.pi * 0.5
if let fixedRotationAngle = self.fixedRotationAngle {
rotationAngle = fixedRotationAngle
}
self.currentOutput = Output(y: yTexture, uv: uvTexture, rotationAngle: rotationAngle, sourceId: self.sourceId)
var resolution = CGSize(width: CGFloat(yTexture.width), height: CGFloat(yTexture.height))
resolution.width = floor(resolution.width * self.sizeMultiplicator.x)
resolution.height = floor(resolution.height * self.sizeMultiplicator.y)
self.currentOutput = Output(resolution: resolution, y: yTexture, uv: uvTexture, rotationAngle: rotationAngle, sourceId: self.sourceId)
return true
}
}

View File

@ -128,8 +128,10 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
private var emojiView: KeyEmojiView?
private var localVideoContainerView: VideoContainerView?
private var remoteVideoContainerView: VideoContainerView?
private let videoContainerBackgroundView: RoundedCornersView
private let overlayContentsVideoContainerBackgroundView: RoundedCornersView
private var videoContainerViews: [VideoContainerView] = []
private var activeRemoteVideoSource: VideoSource?
private var waitingForFirstRemoteVideoFrameDisposable: Disposable?
@ -137,6 +139,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
private var activeLocalVideoSource: VideoSource?
private var waitingForFirstLocalVideoFrameDisposable: Disposable?
private var areControlsHidden: Bool = false
private var swapLocalAndRemoteVideo: Bool = false
private var processedInitialAudioLevelBump: Bool = false
private var audioLevelBump: Float = 0.0
@ -161,6 +166,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
self.blobLayer = CallBlobsLayer()
self.avatarLayer = AvatarLayer()
self.videoContainerBackgroundView = RoundedCornersView(color: .black)
self.overlayContentsVideoContainerBackgroundView = RoundedCornersView(color: UIColor(white: 0.1, alpha: 1.0))
self.titleView = TextView()
self.statusView = StatusView()
@ -169,9 +177,13 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
self.layer.addSublayer(self.backgroundLayer)
self.overlayContentsView.layer.addSublayer(self.backgroundLayer.blurredLayer)
self.overlayContentsView.addSubview(self.overlayContentsVideoContainerBackgroundView)
self.layer.addSublayer(self.blobLayer)
self.layer.addSublayer(self.avatarLayer)
self.addSubview(self.videoContainerBackgroundView)
self.overlayContentsView.mask = self.maskContents
self.addSubview(self.overlayContentsView)
@ -201,6 +213,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
}
self.audioLevelUpdateSubscription = nil
}
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
public required init?(coder: NSCoder) {
@ -245,6 +259,15 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
}
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if self.activeRemoteVideoSource != nil || self.activeLocalVideoSource != nil {
self.areControlsHidden = !self.areControlsHidden
self.update(transition: .spring(duration: 0.4))
}
}
}
public func update(size: CGSize, insets: UIEdgeInsets, screenCornerRadius: CGFloat, state: State, transition: Transition) {
let params = Params(size: size, insets: insets, screenCornerRadius: screenCornerRadius, state: state)
if self.params == params {
@ -259,7 +282,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
self.activeRemoteVideoSource = remoteVideo
} else {
let firstVideoFrameSignal = Signal<Never, NoError> { subscriber in
remoteVideo.updated = { [weak remoteVideo] in
return remoteVideo.addOnUpdated { [weak remoteVideo] in
guard let remoteVideo else {
subscriber.putCompletion()
return
@ -268,12 +291,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
subscriber.putCompletion()
}
}
return EmptyDisposable
}
var shouldUpdate = false
self.waitingForFirstRemoteVideoFrameDisposable = (firstVideoFrameSignal
|> timeout(4.0, queue: .mainQueue(), alternate: .complete())
|> deliverOnMainQueue).startStrict(completed: { [weak self] in
guard let self else {
return
@ -297,7 +317,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
self.activeLocalVideoSource = localVideo
} else {
let firstVideoFrameSignal = Signal<Never, NoError> { subscriber in
localVideo.updated = { [weak localVideo] in
return localVideo.addOnUpdated { [weak localVideo] in
guard let localVideo else {
subscriber.putCompletion()
return
@ -306,12 +326,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
subscriber.putCompletion()
}
}
return EmptyDisposable
}
var shouldUpdate = false
self.waitingForFirstLocalVideoFrameDisposable = (firstVideoFrameSignal
|> timeout(4.0, queue: .mainQueue(), alternate: .complete())
|> deliverOnMainQueue).startStrict(completed: { [weak self] in
guard let self else {
return
@ -328,6 +345,10 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
}
}
if self.activeRemoteVideoSource == nil && self.activeLocalVideoSource == nil {
self.areControlsHidden = false
}
self.params = params
self.updateInternal(params: params, transition: transition)
}
@ -340,34 +361,42 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
}
private func updateInternal(params: Params, transition: Transition) {
let backgroundFrame = CGRect(origin: CGPoint(), size: params.size)
let aspect: CGFloat = params.size.width / params.size.height
let sizeNorm: CGFloat = 64.0
let renderingSize = CGSize(width: floor(sizeNorm * aspect), height: sizeNorm)
let edgeSize: Int = 2
let primaryVideoSource: VideoSource?
let secondaryVideoSource: VideoSource?
if let activeRemoteVideoSource = self.activeRemoteVideoSource, let activeLocalVideoSource = self.activeLocalVideoSource {
primaryVideoSource = activeRemoteVideoSource
secondaryVideoSource = activeLocalVideoSource
} else if let activeRemoteVideoSource = self.activeRemoteVideoSource {
primaryVideoSource = activeRemoteVideoSource
secondaryVideoSource = nil
} else if let activeLocalVideoSource = self.activeLocalVideoSource {
primaryVideoSource = activeLocalVideoSource
secondaryVideoSource = nil
} else {
primaryVideoSource = nil
secondaryVideoSource = nil
let genericAlphaTransition: Transition
switch transition.animation {
case .none:
genericAlphaTransition = .immediate
case let .curve(duration, _):
genericAlphaTransition = .easeInOut(duration: min(0.3, duration))
}
let havePrimaryVideo = self.activeRemoteVideoSource != nil || self.activeLocalVideoSource != nil
let backgroundFrame = CGRect(origin: CGPoint(), size: params.size)
let visualBackgroundFrame = backgroundFrame.insetBy(dx: -CGFloat(edgeSize) / renderingSize.width * backgroundFrame.width, dy: -CGFloat(edgeSize) / renderingSize.height * backgroundFrame.height)
var activeVideoSources: [(VideoContainerView.Key, VideoSource)] = []
if self.swapLocalAndRemoteVideo {
if let activeLocalVideoSource = self.activeLocalVideoSource {
activeVideoSources.append((.background, activeLocalVideoSource))
}
if let activeRemoteVideoSource = self.activeRemoteVideoSource {
activeVideoSources.append((.foreground, activeRemoteVideoSource))
}
} else {
if let activeRemoteVideoSource = self.activeRemoteVideoSource {
activeVideoSources.append((.background, activeRemoteVideoSource))
}
if let activeLocalVideoSource = self.activeLocalVideoSource {
activeVideoSources.append((.foreground, activeLocalVideoSource))
}
}
let havePrimaryVideo = !activeVideoSources.isEmpty
self.backgroundLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(renderingSize.width) + edgeSize * 2, height: Int(renderingSize.height) + edgeSize * 2))
let currentAreControlsHidden = havePrimaryVideo && self.areControlsHidden
let backgroundAspect: CGFloat = params.size.width / params.size.height
let backgroundSizeNorm: CGFloat = 64.0
let backgroundRenderingSize = CGSize(width: floor(backgroundSizeNorm * backgroundAspect), height: backgroundSizeNorm)
let backgroundEdgeSize: Int = 2
let visualBackgroundFrame = backgroundFrame.insetBy(dx: -CGFloat(backgroundEdgeSize) / backgroundRenderingSize.width * backgroundFrame.width, dy: -CGFloat(backgroundEdgeSize) / backgroundRenderingSize.height * backgroundFrame.height)
self.backgroundLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(backgroundRenderingSize.width) + backgroundEdgeSize * 2, height: Int(backgroundRenderingSize.height) + backgroundEdgeSize * 2))
transition.setFrame(layer: self.backgroundLayer, frame: visualBackgroundFrame)
transition.setFrame(layer: self.backgroundLayer.blurredLayer, frame: visualBackgroundFrame)
@ -427,15 +456,17 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
self.speakerAction?()
}), at: 0)
}
self.buttonGroupView.update(size: params.size, buttons: buttons, transition: transition)
let contentBottomInset = self.buttonGroupView.update(size: params.size, insets: params.insets, controlsHidden: currentAreControlsHidden, buttons: buttons, transition: transition)
if case let .active(activeState) = params.state.lifecycleState {
let emojiView: KeyEmojiView
var emojiTransition = transition
var emojiAlphaTransition = genericAlphaTransition
if let current = self.emojiView {
emojiView = current
} else {
emojiTransition = transition.withAnimation(.none)
emojiAlphaTransition = genericAlphaTransition.withAnimation(.none)
emojiView = KeyEmojiView(emoji: activeState.emojiKey)
self.emojiView = emojiView
}
@ -445,11 +476,18 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
emojiView.animateIn()
}
}
emojiTransition.setFrame(view: emojiView, frame: CGRect(origin: CGPoint(x: params.size.width - params.insets.right - 12.0 - emojiView.size.width, y: params.insets.top + 27.0), size: emojiView.size))
let emojiY: CGFloat
if currentAreControlsHidden {
emojiY = -8.0 - emojiView.size.height
} else {
emojiY = params.insets.top + 12.0
}
emojiTransition.setFrame(view: emojiView, frame: CGRect(origin: CGPoint(x: params.size.width - params.insets.right - 12.0 - emojiView.size.width, y: emojiY), size: emojiView.size))
emojiAlphaTransition.setAlpha(view: emojiView, alpha: currentAreControlsHidden ? 0.0 : 1.0)
} else {
if let emojiView = self.emojiView {
self.emojiView = nil
transition.setAlpha(view: emojiView, alpha: 0.0, completion: { [weak emojiView] _ in
genericAlphaTransition.setAlpha(view: emojiView, alpha: 0.0, completion: { [weak emojiView] _ in
emojiView?.removeFromSuperview()
})
}
@ -464,112 +502,162 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
let avatarFrame = havePrimaryVideo ? expandedAvatarFrame : collapsedAvatarFrame
let avatarCornerRadius = havePrimaryVideo ? params.screenCornerRadius : collapsedAvatarSize * 0.5
let minimizedVideoInsets = UIEdgeInsets(top: 124.0, left: 12.0, bottom: 178.0, right: 12.0)
var minimizedVideoInsets = UIEdgeInsets()
minimizedVideoInsets.top = params.insets.top + (currentAreControlsHidden ? 0.0 : 60.0)
minimizedVideoInsets.left = params.insets.left + 12.0
minimizedVideoInsets.right = params.insets.right + 12.0
minimizedVideoInsets.bottom = contentBottomInset + 12.0
if let primaryVideoSource {
let remoteVideoContainerView: VideoContainerView
if let current = self.remoteVideoContainerView {
remoteVideoContainerView = current
var validVideoContainerKeys: [VideoContainerView.Key] = []
for i in 0 ..< activeVideoSources.count {
let (videoContainerKey, videoSource) = activeVideoSources[i]
validVideoContainerKeys.append(videoContainerKey)
var animateIn = false
let videoContainerView: VideoContainerView
if let current = self.videoContainerViews.first(where: { $0.key == videoContainerKey }) {
videoContainerView = current
} else {
remoteVideoContainerView = VideoContainerView(frame: CGRect())
self.remoteVideoContainerView = remoteVideoContainerView
self.insertSubview(remoteVideoContainerView, belowSubview: self.overlayContentsView)
self.overlayContentsView.layer.addSublayer(remoteVideoContainerView.blurredContainerLayer)
animateIn = true
videoContainerView = VideoContainerView(key: videoContainerKey)
switch videoContainerKey {
case .foreground:
self.overlayContentsView.layer.addSublayer(videoContainerView.blurredContainerLayer)
self.insertSubview(videoContainerView, belowSubview: self.overlayContentsView)
self.videoContainerViews.append(videoContainerView)
case .background:
if !self.videoContainerViews.isEmpty {
self.overlayContentsView.layer.insertSublayer(videoContainerView.blurredContainerLayer, below: self.videoContainerViews[0].blurredContainerLayer)
self.insertSubview(videoContainerView, belowSubview: self.videoContainerViews[0])
self.videoContainerViews.insert(videoContainerView, at: 0)
} else {
self.overlayContentsView.layer.addSublayer(videoContainerView.blurredContainerLayer)
self.insertSubview(videoContainerView, belowSubview: self.overlayContentsView)
self.videoContainerViews.append(videoContainerView)
}
}
remoteVideoContainerView.layer.position = self.avatarLayer.position
remoteVideoContainerView.layer.bounds = self.avatarLayer.bounds
remoteVideoContainerView.alpha = 0.0
remoteVideoContainerView.blurredContainerLayer.position = self.avatarLayer.position
remoteVideoContainerView.blurredContainerLayer.bounds = self.avatarLayer.bounds
remoteVideoContainerView.blurredContainerLayer.opacity = 0.0
remoteVideoContainerView.update(size: self.avatarLayer.bounds.size, insets: minimizedVideoInsets, cornerRadius: self.avatarLayer.params?.cornerRadius ?? 0.0, isMinimized: false, isAnimatingOut: false, transition: .immediate)
videoContainerView.pressAction = { [weak self] in
guard let self else {
return
}
self.swapLocalAndRemoteVideo = !self.swapLocalAndRemoteVideo
self.update(transition: .easeInOut(duration: 0.25))
}
}
if remoteVideoContainerView.video !== primaryVideoSource {
remoteVideoContainerView.video = primaryVideoSource
if videoContainerView.video !== videoSource {
videoContainerView.video = videoSource
}
transition.setPosition(view: remoteVideoContainerView, position: expandedVideoFrame.center)
transition.setBounds(view: remoteVideoContainerView, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size))
transition.setAlpha(view: remoteVideoContainerView, alpha: 1.0)
transition.setPosition(layer: remoteVideoContainerView.blurredContainerLayer, position: expandedVideoFrame.center)
transition.setBounds(layer: remoteVideoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size))
transition.setAlpha(layer: remoteVideoContainerView.blurredContainerLayer, alpha: 1.0)
remoteVideoContainerView.update(size: expandedVideoFrame.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, isMinimized: false, isAnimatingOut: false, transition: transition)
} else {
if let remoteVideoContainerView = self.remoteVideoContainerView {
remoteVideoContainerView.update(size: avatarFrame.size, insets: minimizedVideoInsets, cornerRadius: avatarCornerRadius, isMinimized: false, isAnimatingOut: true, transition: transition)
transition.setPosition(layer: remoteVideoContainerView.blurredContainerLayer, position: avatarFrame.center)
transition.setBounds(layer: remoteVideoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
transition.setAlpha(layer: remoteVideoContainerView.blurredContainerLayer, alpha: 0.0)
transition.setPosition(view: remoteVideoContainerView, position: avatarFrame.center)
transition.setBounds(view: remoteVideoContainerView, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
if remoteVideoContainerView.alpha != 0.0 {
transition.setAlpha(view: remoteVideoContainerView, alpha: 0.0, completion: { [weak self, weak remoteVideoContainerView] completed in
guard let self, let remoteVideoContainerView, completed else {
let videoContainerTransition = transition
if animateIn {
if i == 0 && self.videoContainerViews.count == 1 {
videoContainerView.layer.position = self.avatarLayer.position
videoContainerView.layer.bounds = self.avatarLayer.bounds
videoContainerView.alpha = 0.0
videoContainerView.blurredContainerLayer.position = self.avatarLayer.position
videoContainerView.blurredContainerLayer.bounds = self.avatarLayer.bounds
videoContainerView.blurredContainerLayer.opacity = 0.0
videoContainerView.update(size: self.avatarLayer.bounds.size, insets: minimizedVideoInsets, cornerRadius: self.avatarLayer.params?.cornerRadius ?? 0.0, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: .immediate)
} else {
videoContainerView.layer.position = expandedVideoFrame.center
videoContainerView.layer.bounds = CGRect(origin: CGPoint(), size: expandedVideoFrame.size)
videoContainerView.alpha = 0.0
videoContainerView.blurredContainerLayer.position = expandedVideoFrame.center
videoContainerView.blurredContainerLayer.bounds = CGRect(origin: CGPoint(), size: expandedVideoFrame.size)
videoContainerView.blurredContainerLayer.opacity = 0.0
videoContainerView.update(size: self.avatarLayer.bounds.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: i != 0, isAnimatedOut: i != 0, transition: .immediate)
}
}
videoContainerTransition.setPosition(view: videoContainerView, position: expandedVideoFrame.center)
videoContainerTransition.setBounds(view: videoContainerView, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size))
videoContainerTransition.setPosition(layer: videoContainerView.blurredContainerLayer, position: expandedVideoFrame.center)
videoContainerTransition.setBounds(layer: videoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size))
videoContainerView.update(size: expandedVideoFrame.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: i != 0, isAnimatedOut: false, transition: videoContainerTransition)
let alphaTransition: Transition
switch transition.animation {
case .none:
alphaTransition = .immediate
case let .curve(duration, _):
if animateIn {
if i == 0 {
if self.videoContainerViews.count > 1 && self.videoContainerViews[1].isFillingBounds {
alphaTransition = .immediate
} else {
alphaTransition = transition
}
} else {
alphaTransition = .easeInOut(duration: min(0.1, duration))
}
} else {
alphaTransition = transition
}
}
alphaTransition.setAlpha(view: videoContainerView, alpha: 1.0)
alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 1.0)
}
var removedVideoContainerIndices: [Int] = []
for i in 0 ..< self.videoContainerViews.count {
let videoContainerView = self.videoContainerViews[i]
if !validVideoContainerKeys.contains(videoContainerView.key) {
removedVideoContainerIndices.append(i)
if self.videoContainerViews.count == 1 {
let alphaTransition: Transition = genericAlphaTransition
videoContainerView.update(size: avatarFrame.size, insets: minimizedVideoInsets, cornerRadius: avatarCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: transition)
transition.setPosition(layer: videoContainerView.blurredContainerLayer, position: avatarFrame.center)
transition.setBounds(layer: videoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
transition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 0.0)
transition.setPosition(view: videoContainerView, position: avatarFrame.center)
transition.setBounds(view: videoContainerView, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
if videoContainerView.alpha != 0.0 {
alphaTransition.setAlpha(view: videoContainerView, alpha: 0.0, completion: { [weak videoContainerView] _ in
guard let videoContainerView else {
return
}
videoContainerView.removeFromSuperview()
videoContainerView.blurredContainerLayer.removeFromSuperlayer()
})
alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 0.0)
}
} else if i == 0 {
let alphaTransition = genericAlphaTransition
alphaTransition.setAlpha(view: videoContainerView, alpha: 0.0, completion: { [weak videoContainerView] _ in
guard let videoContainerView else {
return
}
remoteVideoContainerView.removeFromSuperview()
remoteVideoContainerView.blurredContainerLayer.removeFromSuperlayer()
if self.remoteVideoContainerView === remoteVideoContainerView {
self.remoteVideoContainerView = nil
}
videoContainerView.removeFromSuperview()
videoContainerView.blurredContainerLayer.removeFromSuperlayer()
})
alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 0.0)
} else {
let alphaTransition = genericAlphaTransition
alphaTransition.setAlpha(view: videoContainerView, alpha: 0.0, completion: { [weak videoContainerView] _ in
guard let videoContainerView else {
return
}
videoContainerView.removeFromSuperview()
videoContainerView.blurredContainerLayer.removeFromSuperlayer()
})
alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 0.0)
videoContainerView.update(size: params.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: true, isAnimatedOut: true, transition: transition)
}
}
}
if let secondaryVideoSource {
let localVideoContainerView: VideoContainerView
if let current = self.localVideoContainerView {
localVideoContainerView = current
} else {
localVideoContainerView = VideoContainerView(frame: CGRect())
self.localVideoContainerView = localVideoContainerView
self.insertSubview(localVideoContainerView, belowSubview: self.overlayContentsView)
self.overlayContentsView.layer.addSublayer(localVideoContainerView.blurredContainerLayer)
localVideoContainerView.layer.position = self.avatarLayer.position
localVideoContainerView.layer.bounds = self.avatarLayer.bounds
localVideoContainerView.alpha = 0.0
localVideoContainerView.blurredContainerLayer.position = self.avatarLayer.position
localVideoContainerView.blurredContainerLayer.bounds = self.avatarLayer.bounds
localVideoContainerView.blurredContainerLayer.opacity = 0.0
localVideoContainerView.update(size: self.avatarLayer.bounds.size, insets: minimizedVideoInsets, cornerRadius: self.avatarLayer.params?.cornerRadius ?? 0.0, isMinimized: true, isAnimatingOut: false, transition: .immediate)
}
if localVideoContainerView.video !== secondaryVideoSource {
localVideoContainerView.video = secondaryVideoSource
}
transition.setPosition(view: localVideoContainerView, position: expandedVideoFrame.center)
transition.setBounds(view: localVideoContainerView, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size))
transition.setAlpha(view: localVideoContainerView, alpha: 1.0)
transition.setPosition(layer: localVideoContainerView.blurredContainerLayer, position: expandedVideoFrame.center)
transition.setBounds(layer: localVideoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size))
transition.setAlpha(layer: localVideoContainerView.blurredContainerLayer, alpha: 1.0)
localVideoContainerView.update(size: expandedVideoFrame.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, isMinimized: true, isAnimatingOut: false, transition: transition)
} else {
if let localVideoContainerView = self.localVideoContainerView {
localVideoContainerView.update(size: avatarFrame.size, insets: minimizedVideoInsets, cornerRadius: avatarCornerRadius, isMinimized: false, isAnimatingOut: true, transition: transition)
transition.setPosition(layer: localVideoContainerView.blurredContainerLayer, position: avatarFrame.center)
transition.setBounds(layer: localVideoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
transition.setAlpha(layer: localVideoContainerView.blurredContainerLayer, alpha: 0.0)
transition.setPosition(view: localVideoContainerView, position: avatarFrame.center)
transition.setBounds(view: localVideoContainerView, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
if localVideoContainerView.alpha != 0.0 {
transition.setAlpha(view: localVideoContainerView, alpha: 0.0, completion: { [weak self, weak localVideoContainerView] completed in
guard let self, let localVideoContainerView, completed else {
return
}
localVideoContainerView.removeFromSuperview()
localVideoContainerView.blurredContainerLayer.removeFromSuperlayer()
if self.localVideoContainerView === localVideoContainerView {
self.localVideoContainerView = nil
}
})
}
}
for index in removedVideoContainerIndices.reversed() {
self.videoContainerViews.remove(at: index)
}
if self.avatarLayer.image !== params.state.avatarImage {
@ -577,7 +665,17 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
}
transition.setPosition(layer: self.avatarLayer, position: avatarFrame.center)
transition.setBounds(layer: self.avatarLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
self.avatarLayer.update(size: collapsedAvatarFrame.size, isExpanded:havePrimaryVideo, cornerRadius: avatarCornerRadius, transition: transition)
self.avatarLayer.update(size: collapsedAvatarFrame.size, isExpanded: havePrimaryVideo, cornerRadius: avatarCornerRadius, transition: transition)
transition.setPosition(view: self.videoContainerBackgroundView, position: avatarFrame.center)
transition.setBounds(view: self.videoContainerBackgroundView, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
transition.setAlpha(view: self.videoContainerBackgroundView, alpha: havePrimaryVideo ? 1.0 : 0.0)
self.videoContainerBackgroundView.update(cornerRadius: havePrimaryVideo ? params.screenCornerRadius : avatarCornerRadius, transition: transition)
transition.setPosition(view: self.overlayContentsVideoContainerBackgroundView, position: avatarFrame.center)
transition.setBounds(view: self.overlayContentsVideoContainerBackgroundView, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
transition.setAlpha(view: self.overlayContentsVideoContainerBackgroundView, alpha: havePrimaryVideo ? 1.0 : 0.0)
self.overlayContentsVideoContainerBackgroundView.update(cornerRadius: havePrimaryVideo ? params.screenCornerRadius : avatarCornerRadius, transition: transition)
let blobFrame = CGRect(origin: CGPoint(x: floor(avatarFrame.midX - blobSize * 0.5), y: floor(avatarFrame.midY - blobSize * 0.5)), size: CGSize(width: blobSize, height: blobSize))
transition.setPosition(layer: self.blobLayer, position: CGPoint(x: blobFrame.midX, y: blobFrame.midY))
@ -606,14 +704,6 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
constrainedWidth: params.size.width - 16.0 * 2.0,
transition: transition
)
let titleFrame = CGRect(
origin: CGPoint(
x: (params.size.width - titleSize.width) * 0.5,
y: !havePrimaryVideo ? collapsedAvatarFrame.maxY + 39.0 : params.insets.top + 17.0
),
size: titleSize
)
transition.setFrame(view: self.titleView, frame: titleFrame)
let statusState: StatusView.State
switch params.state.lifecycleState {
@ -661,6 +751,25 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
}
let statusSize = self.statusView.update(state: statusState, transition: .immediate)
let titleY: CGFloat
if currentAreControlsHidden {
titleY = -8.0 - titleSize.height - statusSize.height
} else if havePrimaryVideo {
titleY = params.insets.top + 2.0
} else {
titleY = collapsedAvatarFrame.maxY + 39.0
}
let titleFrame = CGRect(
origin: CGPoint(
x: (params.size.width - titleSize.width) * 0.5,
y: titleY
),
size: titleSize
)
transition.setFrame(view: self.titleView, frame: titleFrame)
genericAlphaTransition.setAlpha(view: self.titleView, alpha: currentAreControlsHidden ? 0.0 : 1.0)
let statusFrame = CGRect(
origin: CGPoint(
x: (params.size.width - statusSize.width) * 0.5,
@ -678,6 +787,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
}
} else {
transition.setFrame(view: self.statusView, frame: statusFrame)
genericAlphaTransition.setAlpha(view: self.statusView, alpha: currentAreControlsHidden ? 0.0 : 1.0)
}
if case let .active(activeState) = params.state.lifecycleState, activeState.signalInfo.quality <= 0.2 {
@ -690,7 +800,13 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
self.addSubview(weakSignalView)
}
let weakSignalSize = weakSignalView.update(constrainedSize: CGSize(width: params.size.width - 32.0, height: 100.0))
let weakSignalFrame = CGRect(origin: CGPoint(x: floor((params.size.width - weakSignalSize.width) * 0.5), y: statusFrame.maxY + (havePrimaryVideo ? 12.0 : 12.0)), size: weakSignalSize)
let weakSignalY: CGFloat
if currentAreControlsHidden {
weakSignalY = params.insets.top + 2.0
} else {
weakSignalY = statusFrame.maxY + (havePrimaryVideo ? 12.0 : 12.0)
}
let weakSignalFrame = CGRect(origin: CGPoint(x: floor((params.size.width - weakSignalSize.width) * 0.5), y: weakSignalY), size: weakSignalSize)
if weakSignalView.bounds.isEmpty {
weakSignalView.frame = weakSignalFrame
if !transition.animation.isImmediate {

View File

@ -513,7 +513,8 @@ final class PeerAllowedReactionsScreenComponent: Component {
animateAsReplacement = true
}
let undoController = UndoOverlayController(presentationData: presentationData, content: .customEmoji(context: component.context, file: itemFile, loop: false, title: nil, text: presentationData.strings.ChannelReactions_ToastLevelBoostRequired("\(nextCustomReactionCount)", "\(nextCustomReactionCount)").string, undoText: nil, customAction: nil), elevatedLayout: false, position: .bottom, animateInAsReplacement: animateAsReplacement, action: { _ in return false })
let text = presentationData.strings.ChannelReactions_ToastLevelBoostRequiredTemplate(presentationData.strings.ChannelReactions_ToastLevelBoostRequiredTemplateLevel(Int32(nextCustomReactionCount)), presentationData.strings.ChannelReactions_ToastLevelBoostRequiredTemplateEmojiCount(Int32(nextCustomReactionCount))).string
let undoController = UndoOverlayController(presentationData: presentationData, content: .customEmoji(context: component.context, file: itemFile, loop: false, title: nil, text: text, undoText: nil, customAction: nil), elevatedLayout: false, position: .bottom, animateInAsReplacement: animateAsReplacement, action: { _ in return false })
self.currentUndoController = undoController
self.environment?.controller()?.present(undoController, in: .current)
}
@ -724,13 +725,12 @@ final class PeerAllowedReactionsScreenComponent: Component {
self.reactionInput = reactionInput
}
//TOOD:localize
let reactionInputSize = reactionInput.update(
transition: animateIn ? .immediate : transition,
component: AnyComponent(EmojiListInputComponent(
context: component.context,
theme: environment.theme,
placeholder: "Add Reactions...",
placeholder: environment.strings.ChannelReactions_InputPlaceholder,
reactionItems: enabledReactions,
isInputActive: self.displayInput,
caretPosition: caretPosition,